Skip to content

wasapi: detect device changes and handle disconnection gracefully#2538

Open
0x0003 wants to merge 2 commits into
MusicPlayerDaemon:masterfrom
0x0003:win-wasapi
Open

wasapi: detect device changes and handle disconnection gracefully#2538
0x0003 wants to merge 2 commits into
MusicPlayerDaemon:masterfrom
0x0003:win-wasapi

Conversation

@0x0003

@0x0003 0x0003 commented Jul 12, 2026

Copy link
Copy Markdown

Closes #1408
Closes #1880

Changes were made to Control.hxx, Interface.hxx, Thread.cxx, which are technically a bit out of scope as those are generic, but I couldn't figure out how to make it work some other way.
Their behavior essentially stays the same though, so no other platforms should be affected.

0x0003 added 2 commits July 12, 2026 11:06
Catch exceptions from GetVolume()/SetVolume() so that a disconnected
device (AUDCLNT_E_DEVICE_INVALIDATED) degrades gracefully instead of
crashing MPD.

Closes MusicPlayerDaemon#1880
Register an IMMNotificationClient to receive OnDefaultDeviceChanged
callbacks. On change, the output thread wakes and reopens on the new
default device immediately, bypassing the 10-second fail timer via
a new AudioOutput::IsDeviceChange() virtual.

Closes MusicPlayerDaemon#1408
Closes MusicPlayerDaemon#1880
"Unable to get master "
"volume level");
try {
auto future = com_worker->Async([&]() -> int {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You moved this into the try block, but what exception will this catch?

Comment on lines +74 to +75
LogDebug(wasapi_mixer_domain,
"Failed to get volume, device may be disconnected");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a debug message and you're discarding the actual error details.

return;

try {
com_worker->Async([&]() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above. The diff is awfully large and that needs to be justified.

@MaxKellermann MaxKellermann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand much of this because I'm not a Windows guy, but some things looked exceptionally ugly.

const UINT32 frame_size;
const UINT32 buffer_size_in_frames;
const bool is_exclusive;
const bool is_exclusive;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why whitespace here?

* device changes on Windows.
*/
class WasapiDeviceNotification final : public IMMNotificationClient {
std::atomic_bool &device_changed;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A reference to an atomic? That is truly ugly. Can you do better?

Comment on lines +459 to +460
/* Default output device changed; the output
framework will reopen on the new device. */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what if we're not using the default device?

framework will reopen on the new device. */
LogDebug(wasapi_output_domain,
"Audio device changed, stopping playback");
throw std::runtime_error("Audio device changed");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that an error?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To trigger InternalCloseError and IsDeviceChange(), that's why the latter was introduced in the first place. It's not a "real" error. Without it there's a few seconds worth of delay (REOPEN_AFTER in Control.cxx) before audio stream resumes after another device disconnects.
Changing it would require adding a new reopen mechanism to the output framework. Maybe I'm just dumb and there's better/simpler way, but this is the best I could come up with.

HANDLE handles[2] = { event.handle(), device_event };
WaitForMultipleObjects(2, handles, FALSE, INFINITE);

if (device_changed.load(std::memory_order_acquire)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you'd use the return value of WaitForMultipleObjects(), this could be implemented without the atomic_bool, couldn't it?

Comment thread src/output/Interface.hxx
* output framework may use this to skip the fail timer and
* reopen the output immediately.
*/
virtual bool IsDeviceChange() const noexcept {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not an elegant design. If you really wanted to signal this kind of thing, you'd better throw a certain exception class, this would still be ugly but somewhat less ugly. But I have a feeling a better design would be possible.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment on WasapiOutputPlugin.cxx:460-463. I'll see if I can figure out another way, but I'm kind of at my wit's end here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants